<!doctype html>
<html>
<head>
<script src="jquery.js"></script>
<script src="essentials.js"></script>
<script>
/* the container of global data, shared by all tabs */
var _heap = {};

const LEFT = E.LEFT;
const RIGHT = E.RIGHT;
const UP = E.UP;
const DOWN = E.DOWN;

/* all living tabs, including recently closed ones */
var _tabs = {
	/* number of revivable tabs */
	SIZE: 10,
	/* the stack of revivable tabs' id */
	closed: [],
	/* all tabs, opened and revivables */
	all: {},
	/* should be called when a tab's URL is updated */
	update: function(tab) {
//		if (cjs_avail(tab.url))
		/* damn! I have to record history manually */
		var old = _tabs.all[tab.id];

		if (!old)
			tab.visited = [tab.url];
		else if (old.visited[old.visited.length - 1] != tab.url)
			tab.visited = old.visited.concat(tab.url);
		else
			tab.visited = old.visited;

		_tabs.all[tab.id] = tab;
	},
	/* called when a tab reports an info update */
	info_updated: function(tab, info) {
		if (!!info.history) {
			_tabs.all[tab.id].visited.splice(info.history);
		}
	},
	/* should be called when a tab is remove */
	remove: function(id) {
		/* push to the stack of recently closed tabs */
		if (!!_tabs.all[id]) {
			_tabs.closed.push(id);
			while (_tabs.SIZE < _tabs.closed.length) {
				/* discard tabs closed earlier */
				delete _tabs.all[_tabs.closed.splice(0, 1)[0]];
			}
		}
	}
};

/* effective config */
var _cfg;

/* default config */
var _defcfg = {
	'version': 1.951,
	'trigger': E.WIN ? E.RBUTTON:$.MBUTTON,	// mouse button to trigger stroke
	'timeout': 1000,			// a stroke sequence expires after timeout (milliseconds)
	'minstep': 20,				// minimum moving distance of a stroke (in px)
	'trail': 'black',			// show trails when doing mouse strokes
	'trail_width': 1,			// trail width
	'newtab_target': 'chrome://newtab/',	// the page to open when issue a new tab command
	'newtab_position': '+1',		// where to put the new tab
	'suppress': true,			// suppress stroke actions when holding ALT
	'last_tab_close_win': true,		// when close the last tab, also close the window
	'custom_req_ctrlkey': true,		// custom strokes require ctrl key
	'enable_dnd': true,			// enable drag and drop strokes
	'enable_animation': true,		// enable animations (smooth scroll etc.)
	'search_engine': 'http://www.google.com/search?q=',
	'blacklist': '',
	'categories': {
		'navigation': {
			'to-page-top': [LEFT, UP],
			'to-page-bottom': [LEFT, DOWN],
			'scroll-up-one-page': [],
			'scroll-down-one-page': [],
			'history-back': [LEFT],
			'history-forward': [RIGHT],
			'previous-tab': [UP, LEFT],
			'next-tab': [UP, RIGHT],
			'first-tab': [],
			'last-tab': [],
			'upper-level-in-url': [DOWN, UP],
			'increase-number-in-url': [RIGHT, UP, RIGHT, UP],
			'decrease-number-in-url': [RIGHT, DOWN, RIGHT, DOWN],
			'open-homepage': []
		},

		'window': {
			'new-window': [DOWN, RIGHT, UP],
			'close-window': [UP, RIGHT, DOWN],
			//'minimize-window': [DOWN, LEFT],
			'maximize-window': [RIGHT, DOWN, UP]
		},

		'tab': {
			'new-tab': [LEFT, RIGHT],
			'close-tab': [DOWN, RIGHT],
			'undo-close-tab': [RIGHT, LEFT],
			'detach-tab': [],
			'duplicate-tab': [],
			'close-tab-to-the-left': [],
			'close-tab-to-the-right': [],
			'close-all-tabs-to-the-left': [],
			'close-all-tabs-to-the-right': [],
			'close-other-tabs': []
		},

		'link': {
			'open-link-in-new-window': [],
			'open-link-in-new-background-tab': [DOWN],
			'open-link-in-new-foreground-tab': [UP],
			'bookmark-this-link': []
		},

		'image': {
			//'save-image': [],
			'view-image': []
		},

		'bookmark': {
			'bookmark-this-page': [DOWN, RIGHT, UP, LEFT],
			'remove-bookmark': [LEFT, DOWN, RIGHT, UP]
		},

		'utilities': {
			'reload': [UP, DOWN],
			'skip-cache-reload': [UP, DOWN, UP],
			'stop-page-loading': [],
			'text-zoom-in': [LEFT, RIGHT, UP],
			'text-zoom-out': [LEFT, RIGHT, DOWN],
			'text-zoom-reset': [LEFT, RIGHT, UP, DOWN],
			'search-selected-text': [],
			'view-source': [LEFT, DOWN, RIGHT, DOWN, LEFT],
			'take-screenshot': [RIGHT, LEFT, RIGHT, LEFT]
		},

		'other': {
			'mouse-stroke-options': [DOWN, UP, RIGHT, DOWN]
		}
	},
	'customs': {
		/* custom strokes */
		'userdef#Hide%20all%20images%20%28example%29': [UP],
		'userdef#Search%20in%20YouTube%20%28example%29': [DOWN]
	},
	'scripts': {
		/* scripts of custom strokes */
		'userdef#Hide%20all%20images%20%28example%29': {
			frame: 'all',
			script: "var imgs = document.getElementsByTagName('img');\nfor (var i = imgs.length - 1; i >= 0; --i) {\n\timgs[i].parentNode.removeChild(imgs[i])\n}"
		},
		'userdef#Search%20in%20YouTube%20%28example%29': {
			frame: 'top',
			script: "location.href = 'http://www.youtube.com/results?search_query=' + _env.seltext;"
		}
	},
	'rockers': {
		'R': 'next-tab',
		'L': 'previous-tab'
	},
	'wheels': {
		'U': 'text-zoom-in',
		'D': 'text-zoom-out',
		'L': '',
		'R': ''
	},
	'drags': {
		'U': 'open-link-in-new-window',
		'D': 'bookmark-this-link',
		'L': 'open-link-in-new-background-tab',
		'R': 'open-link-in-new-foreground-tab'
	}
};

/* compare stroke sequences */
var match = function(seq1, seq2) {
	if (seq1 === seq2 || !seq1 && !seq2)
		return true;
	if (!seq1 && seq2 || seq1 && !seq2 || seq1.length != seq2.length)
		return false;

	return seq1.join(' ') == seq2.join(' ');
}

/* execute the given script in the given tab */
var tab_exec = function(tab, arg) {
	chrome.tabs.sendRequest(tab.id, {
		type: 'local-eval', data: arg
	});
}

/* test if content scripts are injected in the given url */
var cjs_avail = function(url) {
	return /^(http|https|ftp|file):/i.test(url) &&
		/* no https//chrome.google.com/extensions */
		!/^https?:\/\/chrome\.google\.com\/extensions/i.test(url) ||
		/* options.html also has content scripts */
		0 == url.indexOf(E.OPTION_URL);
}

/* mirror object o2 to object o1, all defined properties values of o2 are
 * copied to o1, properties which are not in o1 are ignored, properties in
 * the mandatory list, if defined, are copied */
var mirror = function(o1, o2, whitelist) {
	if (iterable(o1) && iterable(o2)) {
		/* build list */
		whitelist = !!whitelist ? [].concat(whitelist) : [];
		for (var k in o1)
			if (-1 != whitelist.indexOf(k) && undefined != o2[k])
				o1[k] = o2[k];
			else
				o1[k] = mirror(o1[k], o2[k], whitelist);
	} else if (undefined != o2 && !iterable(o1) && !iterable(o2)) {
		o1 = o2;
	}
	return o1;
}

/* is object but not array */
var iterable = function(o) {
	return !!o && o.constructor == {}.constructor;
}

/* translate older version configurations to the newest version */
var sync_config = function(cfg) {
	var k, k2;

	/* merge with _defcfg, but keep custom stroke settings */
	cfg = mirror(E.copy(_defcfg), cfg, ['customs', 'scripts']);

	/* flat stroke categories for easy referring */
	cfg.strokes = {};
	for (k in cfg.categories) {
		for (k2 in cfg.categories[k]) {
			cfg.strokes[k2] = cfg.categories[k][k2];
		}
	}

	/* version hacks */
	/* prior to 1.9.4.4, _cfg.scripts are plain strings */
	if (cfg.version < 1.944)
		for (k in cfg.scripts)
			if (typeof '' == typeof cfg.scripts[k])
				cfg.scripts[k] = {
					frame: '',
					script: cfg.scripts[k]
				}

	return cfg;
}

/* load user config */
var load_config = function(update_tabs) {
	try {
		if (!(_cfg = JSON.parse(localStorage.getItem('config')))) {
			_cfg = _defcfg;
		}
	} catch (ex) {
		console.error(ex.stack);
		_cfg = _defcfg;
	}

	if (!_cfg.version || !_cfg.strokes || _cfg.version < _defcfg.version) {
		/* just installed or upgraded */
		_cfg = sync_config(_cfg);
		save_config(_cfg);
		version_highlight();
	}

	if (!update_tabs) return;

	/* update config in all tabs */
	for (var id in _tabs.all) {
		if (-1 == _tabs.closed.indexOf(id))
			chrome.tabs.sendRequest(_tabs.all[id].id, {
				type: 'update-config', data: _cfg
			});
	}
}

/* save user config */
var save_config = function(cfg, check)
{
	if (check) {
		cfg = sync_config(cfg);
		cfg.version = _defcfg.version;
	}
	localStorage.setItem('config', JSON.stringify(cfg));
	_cfg = cfg;
}

/* show version highlight */
var version_highlight = function()
{
	if (false && !!webkitNotifications) {
		var n = webkitNotifications.createHTMLNotification('highlight.html');
		n.show();
		_cfg.version = _defcfg.version;
		save_config(_cfg);
		return;
	}

//	var html = _('text_version_highlight_prompt', chrome.extension.getURL('options.html'));
	var html = '<iframe style="border:none;margin:0;padding:0;width:100%" src="' + chrome.extension.getURL('highlight.html') + '"></iframe>';

	var send = function(id, info) {
		chrome.tabs.get(id, function(tab) {
			if (!cjs_avail(tab.url)) return;

			chrome.tabs.sendRequest(id, {
				type: 'show-message', data: { html: html }
			}, function() {
				/* some tabs may not have content scripts
				 * injected, so we only update version number
				 * after we hear the response from the tab */
				_cfg.version = _defcfg.version;
				save_config(_cfg);
				chrome.tabs.onSelectionChanged.removeListener(send);
			});
		});
	}
	chrome.tabs.onSelectionChanged.addListener(send);
}

/* process stroke */
var process = function(msg, tab) {
	var id, action, custom, event = msg.data, stroke = event.stroke;

	event.visited = _tabs.all[tab.id].visited;

	if ('stroke' == event.type) {
		/* if ctrl key is required for custom strokes, search
		 * _cfg.customs only when ctrl key is pressed, otherwise
		 * search _cfg.strokes only;
		 * if ctrl key is not required for custom strokes, ignore
		 * ctrl key status, search _cfg.customs first, if no matches
		 * were found, search _cfg.strokes. */
		if (!_cfg.custom_req_ctrlkey || event.initevt.ctrlKey)
			for (id in _cfg.customs)
				if (match(stroke, _cfg.customs[id])) {
					action = _cfg.scripts[id];
					custom = true;
					break;
				}

		if (_cfg.custom_req_ctrlkey && !event.initevt.ctrlKey ||
			!_cfg.custom_req_ctrlkey && !action)
			for (id in _cfg.strokes)
				if (match(stroke, _cfg.strokes[id])) {
					action = E.actions[id];
					custom = false;
					break;
				}
	} else if ('drag' == event.type) {
		var el = event.initevt.target;
		if (el.href || el.src) {
			action = E.actions[_cfg.drags[event.drag]];
			if (!el.href) el.href = el.src;
		}
		custom = false;
	} else {
		id = 'wheel' == event.type ?
			_cfg.wheels[event.wheel] :
			'rocker' == event.type ?
			_cfg.rockers[event.rocker] :
			null;
		if (!!(action = E.actions[id])) {
			custom = false;
		} else if (!!(action = _cfg.scripts[id])) {
			custom = true;
		}
	}

	if (!action) return;

	/* wait a short period of time for the content script to finish its
	 * work (update button status, clear trail, etc.), there are some
	 * button sync problems without this wait, for example, tab switched
	 * but the stroke trigger key is still engaged
	 * ISSUE=98, ISSUE=30 */
	setTimeout(function() {
		if (custom || action.local) {
			/* exec locally in content script */
			tab_exec(tab, {
				id: id, tab: tab, event: event, custom: custom,
				script: custom ? action : ''
			});
		} else {
			/* call directly */
			event.cfg = E.copy(_cfg);
			action.handler.call(tab, tab, event);
		}
	}, 100);
}

/* synchronize global variables between tabs */
chrome.tabs.onSelectionChanged.addListener(function(id, info) {
	/* share global variables between tabs, this is how it works:
	 * when tabs got selected, the _heap variable (contains all global
	 * data) is injected into it; tabs can read _heap like normal variable,
	 * but when they update _heap, the background page must be notified
	 * (by receiving 'set-global' messages), so changes can be reflected
	 * to other tabs */
	chrome.tabs.get(id, function(tab) {
		var heap = _heap;

		/* assume the tab is not under our control */
		_heap = {};
		console.log('on select: %s [%s]', heap.rdown, tab.url);
		chrome.tabs.sendRequest(id, {
			type: 'put-globals', data: heap
		}, function() {
			/* the tab responded, it's not an internal page nor
			 * blocked, so restore _heap */
			_heap = heap;
		});
	});
});

/* routine to execute when a tab is created */
chrome.tabs.onCreated.addListener(function(tab) {
	/* record all opened tabs */
	_tabs.update(tab);
});

/* routine to execute when a tab is updated */
chrome.tabs.onUpdated.addListener(function(id, info, tab) {
	/* only add to _tabs when url is updated, note that if url is
	 * specified when open a new tab, no onUpdate events will contain
	 * info.url, in such cases, we need to update _tabs in onCreated
	 * events
	 * ISSUE=28 */
	if (info.url) _tabs.update(tab);
});

/* routine to execute when a tab is destroied */
chrome.tabs.onRemoved.addListener(function(id) {
	_tabs.remove(id);
});

/* listen to the content script */
chrome.extension.onConnect.addListener(function(port) {
	if ('stroke.html-stroke' != port.name) return;

	port.onMessage.addListener(function(msg) {
		var tab = port.sender.tab;

		if ('pull-config' == msg.type) {
			/* stroke.js is requesting for _cfg */
			chrome.tabs.sendRequest(tab.id, {
				type: 'update-config', data: _cfg
			});
			console.log('update-config sent to: ' + tab.id);
			return;
		} else if ('report-info' == msg.type) {
			_tabs.info_updated(tab, msg.data);
			return;
		} else if ('get-globals' == msg.type) {
			console.log('get-globals %s [%s]', _heap.rdown, tab.url);
			chrome.tabs.sendRequest(tab.id, {
				type: 'put-globals', data: _heap
			});
			return;
		} else if ('one-click-install' == msg.type) {
			chrome.tabs.create({
				selected: true, url: E.OPTION_URL
			}, function(tab) {
				setTimeout(function() {
					chrome.tabs.sendRequest(tab.id, msg);
				}, E.CLOSE_WAIT);
			});
			return;
		} else if ('set-global' == msg.type) {
			_heap[msg.data.name] = msg.data.value;
			/* When using wheel strokes to navigate from tab A to
			 * tab B, the 'mouseup' event of the right mouse button
			 * is always fired from tab A, where the 'mousedown'
			 * event occured, while the currently selected tab
			 * have been changed to tab B, it causes tab B's _heap
			 * be un-synced with the global _heap because _heap
			 * syncs happen on chrome.tabs.onSelected events, which
			 * occur before the 'mouseup' events be fired, so
			 * we need to re-sync _heap of tab B here
			 * ISSUE=30 */
			chrome.tabs.getSelected(null, function(sel) {
				if (tab.id != sel.id && cjs_avail(sel.url)) {
					console.log('set-globals %s, [%s]', _heap.rdown, tab.url, sel.url);
					chrome.tabs.sendRequest(sel.id, {
						type: 'put-globals',
						data: _heap
					});
				}
			});
			return;
		} else if ('stroke' != msg.type) return;

		if (0 == tab.url.indexOf(E.OPTION_URL)) {
			/* ask the options page if it wants to accept strokes,
			 * when in setting mode (the black screen), options
			 * page process strokes by itself */
			chrome.tabs.sendRequest(tab.id, {
				type: 'accept-stroke',
				data: msg.data
			}, function(resp) {
				if (true === resp) {
					chrome.tabs.get(tab.id, function(tab) {
						process(msg, tab);
					});
				}
			});
		} else {
			/* make sure the stroke handler has the up to date tab
			 * object, port.sender.tab is not always up to date */
			chrome.tabs.get(tab.id, function(tab) {
				process(msg, tab);
			});
		}
	});
});

/* listen to the config page */
chrome.extension.onConnect.addListener(function(port) {
	if ('stroke.html-config' != port.name) return;

	port.onMessage.addListener(function(msg) {
		if ('reload' == msg.type) {
			/* asks all tabs to update config */
			load_config(true);
		} else if ('save' == msg.type) {
			/* save config locally */
			save_config(msg.data, true);
			load_config(true);
		}
	});
});

/* provide some APIs for custom stroke */
chrome.extension.onRequest.addListener(function(msg, sender, callback) {
	if ('remote-eval' !== msg.type) return;

	var api = msg.data.api, arg = msg.data.arg, action,
		valid_pos = function(id, def) {
			return isNaN(id) || id <= 0 ? def : id;
		},
		valid_str = function(str, def) {
			return !!str ? '' + str : def;
		}

	try {
		if ('builtin' === api) {
			/* call built-in stroke handlers */
			action = E.actions[arg.name];
			if (action.local) {
				tab_exec(arg.tab, {
					id: arg.name, tab: arg.tab,
					event: arg.event
				});
			} else {
				arg.event.cfg = E.copy(_cfg);
				action.handler.call(arg.tab, arg.tab, arg.event);
			}
			callback({});
		} else if ('clipboard.copy' === api) {
			var proxy = $('#clipboard-proxy'), copee = valid_str(arg);
			if (copee) {
				proxy.val(copee);
				proxy.select();
			}
			callback({ success: !!copee && document.execCommand('copy') });
		} else if ('tabs.create' === api) {
			chrome.tabs.create({
				windowId: valid_pos(arg.windowId),
				index: valid_pos(arg.index),
				url: valid_str(arg.url),
				selected: !!arg.selected
			}, function(tab) {
				callback(tab);
			});
		} else if ('tabs.remove' === api) {
			chrome.tabs.remove(valid_pos(arg, sender.tab.id), function() {
				callback({});
			});
		} else if ('windows.create' === api) {
			chrome.windows.create({
				url: valid_str(arg.url),
				left: valid_pos(arg.left),
				top: valid_pos(arg.top),
				width: valid_pos(arg.width),
				height: valid_pos(arg.height),
				incognito: !!arg.incognito,
				type: valid_str(arg.type)
			}, function(wnd) {
				callback(wnd);
			});
		} else if ('windows.remove' === api) {
			chrome.windows.remove(valid_pos(arg, sender.tab.windowId), function() {
				callback({});
			});
		}
	} catch (e) {
		console.error(e.stack);
		chrome.tabs.sendRequest(id, {
			type: 'show-message', data: { html: e.message }
		});
		callback({});
	}
});


/* scan all tabs at startup */
chrome.windows.getAll({ populate: true }, function(wnds) {
	var i, j, tabs, tab;
	for (i = 0; i < wnds.length; ++i) {
		for (j = 0, tabs = wnds[i].tabs; j < tabs.length; ++j) {
			tab = tabs[j];
			_tabs.update(tab);
			/* ask the tab to reconnect, so the user won't need
			 * to refresh the webpage after update or re-enable */
			chrome.tabs.sendRequest(tab.id, {
				type: 'connect-port'
			});
		}
	}
});

/* always refresh _defcfg */
localStorage.setItem('def_config', JSON.stringify(_defcfg));
/* get user config */
load_config();
</script>
</head>
<body>
<textarea id="clipboard-proxy"></textarea>
</body>
</html>
